#!/usr/bin/env lua

-- If you need filesystem access, use nixio.fs
local fs = require "nixio.fs"

-- LuCI JSON is used for checking the arguments and converting tables to JSON.
local jsonc = require "luci.jsonc"

-- Nixio provides syslog functionality
local nixio = require "nixio"

-- To access /etc/config files, use the uci module
local UCI = require "luci.model.uci"

-- Slight overkill, but leaving room to do log_info etcetera.
local function log_to_syslog(level, message) nixio.syslog(level, message) end

local function log_error(message)
    log_to_syslog("err", "[luci.example]: " .. message)
end

local function using_uci_directly(section)
    -- Rather than parse files in /etc/config, you can rely on the
    -- luci.model.uci module.
    local uci = UCI.cursor()

    -- https://openwrt.github.io/luci/api/modules/luci.model.uci.html
    local config_name = uci:get("example", section)

    uci.unload("example")

    if not config_name then
        local msg = "'" .. section .. "' not found in /etc/config/example"
        -- Send the log message to syslog so it can be found with logread
        log_error(msg)

        -- Convert a lua table into JSON notation and print to stdout
        -- .stringify() is equivalent to cjson's .encode()
        print(jsonc.stringify({uci_error = msg}))

        -- Indicate failure in the return code
        os.exit(1)
    end

    return config_name
end

-- The methods table defines all of the APIs to expose to rpcd.
-- rpcd will execute this Lua file with the 'list' argument to discover the
-- method names that can be presented over ubus, as well as any arguments
-- those methods take.
local methods = {
    -- How to call this API:
    -- echo '{"section": "first"}' | lua /usr/libexec/rpcd/luci.example call get_uci_value
    -- echo '{"section": "does_not_exist"}' | lua /usr/libexec/rpcd/luci.example call get_uci_value
    get_uci_value = {
        -- Args are specified as a table, where the argument type is specified by example
        -- The value is not used as a default.
        args = {section = "a_string"},
        -- A special key of 'call' points to a function definition for execution.
        call = function(args)
            -- A table for the result.
            local r = {}
            r.result = jsonc.stringify({
                example_section = using_uci_directly(args.section)
            })
            -- The 'call' handler will refer to '.code', but also defaults if not found.
            r.code = 0
            -- Return the table object; the call handler will access the attributes
            -- of the table.
            return r
        end
    },
    -- How to call this API:
    -- echo '{}' | lua /usr/libexec/rpcd/luci.example call get_sample1
    -- ubus call luci.example get_sample1
    get_sample1 = {
        call = function()
            local r = {}
            -- This structure does not map well to a JSONMap in the LuCI form setup.
            -- It can be rendered as a table easily enough with loops.
            r.result = jsonc.stringify({
                num_cats = 1,
                num_dogs = 2,
                num_parakeets = 4,
                is_this_real = false,
                not_found = nil
            })
            return r
        end
    },
    -- How to call this API:
    -- echo '{}' | lua /usr/libexec/rpcd/luci.example call get_sample2
    -- ubus call luci.example get_sample2
    get_sample2 = {
        call = function()
            local r = {}
            -- This is the structural data that JSONMap will work with in the JS file
            local data = {
                option_one = {
                    name = "Some string value",
                    value = "A value string",
                    parakeets = {"one", "two", "three"},
                },
                option_two = {
                    name = "Another string value",
                    value = "And another value",
                    parakeets = {3, 4, 5},
                }
            }
            r.result = jsonc.stringify(data)
            return r
        end
    }
}

local function parseInput()
    -- Input parsing - the RPC daemon calls the Lua script and
    -- sends input to it via stdin, not as an argument on the CLI.
    -- Thus, any testing via the lua interpreter needs to be in the form
    -- echo '{jsondata}' | lua /usr/libexec/rpcd/script call method_name
    local parse = jsonc.new()
    local done, err

    while true do
        local chunk = io.read(4096)
        if not chunk then
            break
        elseif not done and not err then
            done, err = parse:parse(chunk)
        end
    end

    if not done then
        print(jsonc.stringify({
            error = err or "Incomplete input for argument parsing"
        }))
        os.exit(1)
    end

    return parse:get()
end

local function validateArgs(func, uargs)
    -- Validates that arguments picked out by parseInput actually match
    -- up to the arguments expected by the function being called.
    local method = methods[func]
    if not method then
        print(jsonc.stringify({error = "Method not found in methods table"}))
        os.exit(1)
    end

    -- Lua has no length operator for tables, so iterate to get the count
    -- of the keys.
    local n = 0
    for _, _ in pairs(uargs) do n = n + 1 end

    -- If the method defines an args table (so empty tables are not allowed),
    -- and there were no args, then give a useful error message about that.
    if method.args and n == 0 then
        print(jsonc.stringify({
            error = "Received empty arguments for " .. func ..
                " but it requires " .. jsonc.stringify(method.args)
        }))
        os.exit(1)
    end

    uargs.ubus_rpc_session = nil

    local margs = method.args or {}
    for k, v in pairs(uargs) do
        if margs[k] == nil or (v ~= nil and type(v) ~= type(margs[k])) then
            print(jsonc.stringify({
                error = "Invalid argument '" .. k .. "' for " .. func ..
                    " it requires " .. jsonc.stringify(method.args)
            }))
            os.exit(1)
        end
    end

    return method
end

if arg[1] == "list" then
    -- When rpcd starts up, it executes all scripts in /usr/libexec/rpcd
    -- passing 'list' as the first argument. This block of code examines
    -- all of the entries in the methods table, and looks for an attribute
    -- called 'args' to see if there are arguments for the method.
    --
    -- The end result is a JSON struct like
    -- {
    --   "api_name": {},
    --   "api2_name": {"host": "some_string"}
    -- }
    --
    -- Which will be converted by ubus to 
    --  "api_name":{}
    --  "api2_name":{"host":"String"}
    local _, rv = nil, {}
    for _, method in pairs(methods) do rv[_] = method.args or {} end
    print((jsonc.stringify(rv):gsub(":%[%]", ":{}")))
elseif arg[1] == "call" then
    -- rpcd will execute the Lua script with a first argument of 'call',
    -- a second argument of the method name, and a third argument that's
    -- stringified JSON.
    --
    -- To debug your script, it's probably easiest to start with direct
    -- execution, as calling via ubus will hide execution errors. For example:
    -- echo '{}' | lua /usr/libexec/rpcd/luci.example call get_sample2
    --
    -- or
    --
    -- echo '{"section": "firstf"}' | /usr/libexec/rpcd/luci.example call get_uci_value
    --
    -- See https://openwrt.org/docs/techref/ubus for more details on using
    -- ubus to call your RPC script (which is what LuCI will be doing).
    local args = parseInput()
    local method = validateArgs(arg[2], args)
    local run = method.call(args)
    -- Use the result from the table which we know to be JSON already.
    -- Anything printed on stdout is sent via rpcd to the caller. Use
    -- the syslog functions, or logging to a file, if you need debug
    -- logs.
    print(run.result)
    -- And exit with the code supplied.
    os.exit(run.code or 0)
elseif arg[1] == "help" then
    local helptext = [[
Usage:

 To see what methods are exported by this script:

    lua luci.example list

 To call a method that has no arguments:

    echo '{}' | lua luci.example call method_name

 To call a method that takes arguments:

    echo '{"valid": "json", "argument": "value"}' | lua luci.example call method_name

 To call this script via ubus:

    ubus call luci.example method_name '{"valid": "json", "argument": "value"}'
]]
    print(helptext)
end
